/* targetcomms.cpp
 * Class TargetComms
 * Communications thread
 * This class is the ONLY class allowed to communicate directly with the device
 * Done in a separate thread to avoid unduly blocking the GUI (like the old test harness does)
 */

#include <QDateTime>
#include <QFile>

#include "qextserialport.h"
#include "qextserialenumerator.h"
#include "targetcomms.h"

#define MAX_ERRORS  (5)

TargetComms::TargetComms(QObject *parent) : QThread(parent)
{
    //Port settings specific to the S100 device
    PortSettings targetSettings;
    targetSettings.BaudRate = BAUD115200;
    targetSettings.FlowControl = FLOW_HARDWARE;
    targetSettings.Parity = PAR_NONE;
    targetSettings.DataBits = DATA_8;
    targetSettings.StopBits = STOP_1;
    targetSettings.Timeout_Millisec = 200;
    target = new QextSerialPort(targetSettings, QextSerialPort::EventDriven);
    //Tells the port to emit readyRead every time a LF (ascii #10) character arrives
    target->setEventChar('\x0A');

    registerList = new QList<RegisterDef>();

    quit = false;
    targetFound = false;
    tryConnectionAgain = false;
    logStatus = false;
    gotRegisterList = false;
    dumpNeeded = false;
    errorCounter = 0;

    mutex = new QMutex(QMutex::NonRecursive);
    fileMux = new QMutex(QMutex::NonRecursive);
}

TargetComms::~TargetComms()
{
    mutex->lock();
    quit = true;
    mutex->unlock();

    if (isRunning())
        condition.wakeAll();
    wait();

    if (target->isOpen())
        target->close();
    else
        target->terminate();
    delete target;
    delete registerList;

    delete mutex;
    delete fileMux;
}

//Queues a read for ALL the registers - be very careful where you call this, takes a long time
// called from main GUI thread
void TargetComms::queueAll() {

    mutex->lock();
        for (int i=0; i < registerList->count(); i++) {
            RegisterDef reg = registerList->at(i);
            readQueue.append(reg);
    }
    mutex->unlock();

    //if the comms thread for some reason isn't running, start it, otherwise wake it up - its got work to do
    if (!isRunning()) {
        start();
    } else {
        condition.wakeOne();
    }
}

//Queues a read for a specific register address
// called from main GUI thread
void TargetComms::queueRead(int address) {

    mutex->lock();
    RegisterDef reg = registerList->at(registerList->indexOf(RegisterDef(address)));
    readQueue.append(reg);
    mutex->unlock();

    if (!isRunning()) {
        start();
    } else {
        condition.wakeOne();
    }
}

//Queues a write for a specific register address
// called from main GUI thread
void TargetComms::queueWrite(int address, QString value) {

    mutex->lock();
    RegisterDef reg = registerList->at(registerList->indexOf(RegisterDef(address)));
    reg.setValue(value);
    writeQueue.append(reg);
    mutex->unlock();

    if (!isRunning()) {
        start();
    } else {
        condition.wakeOne();
    }
}

//Provides a *pointer* to the master register list, not the list itself
// if we don't already have it, queue a request to fetch it from the device
// called from the main GUI thread
QList<RegisterDef>* TargetComms::getRegisterList()
{
    mutex->lock();
    if (registerList->isEmpty()) {
        gotRegisterList = false;
    } else {
        gotRegisterList = true;
    }
    mutex->unlock();

    if (!isRunning()) {
        start();
    } else {
        condition.wakeOne();
    }
    return registerList;
}

void TargetComms::requestConnect()
{    
    if (targetFound) {
        return;
    } else {
        mutex->lock();
        targetFound = false;
        tryConnectionAgain = true;
        mutex->unlock();

        if (!isRunning()) {
            start();
        } else {
            condition.wakeOne();
        }
    }
}

void TargetComms::setLogStatus(bool status)
{
    bool prevStatus = logStatus;

    //If the status did actually change, set up the files etc.
    if ((status != prevStatus) && status) {
        QDateTime time = QDateTime::currentDateTime();
        QString logFileName = QString("harnesslog_%1.fsl").arg(time.toString("yyyyMMdd_hhmmss"));
        setupLogFile(logFileName);
    }

    mutex->lock();
    logStatus = status;
    mutex->unlock();

    //If the logging has been turned off, wait for the change in status to be read by the comms thread
    // otherwise the last line in the log is incomplete.
    if ((status != prevStatus) && !status) {
        msleep(2500);
        closeLogFile();
    }
}

void TargetComms::dumpLogData()
{
    //Set up a file to receive the data - note: a dump cannot be done while the harness logger is on!
    QDateTime time = QDateTime::currentDateTime();
    QString logFileName = QString("flashlog_%1.fsl").arg(time.toString("yyyyMMdd_hhmmss"));
    setupLogFile(logFileName);

    mutex->lock();
    dumpNeeded = true;
    mutex->unlock();
}

bool TargetComms::setupLogFile(QString logFileName)
{
    QMutexLocker lock(fileMux);
    logFile = new QFile(logFileName);
    if (!logFile->exists()) {
        if (logFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
            logData.setDevice(logFile);
            return true;
        }
    }
    return false;
}

/* ^ functions called from main GUI thread only */

//called either from main thread for harness logging or from the comms thread for flash dump
void TargetComms::closeLogFile()
{
    QMutexLocker lock(fileMux);
    if (logFile) {
        logData.flush();
        logFile->close();
        delete logFile;
    }
}

 /* functions called from comms thread only */

bool TargetComms::connectTarget()
{
    //Cycle through the ports available on the system, sending the connect command to each one
    // when a suitable response is received, we've found the system
    QList<QextPortInfo> ports = QextSerialEnumerator::getPorts();
    foreach (QextPortInfo port, ports) {
        targetPortName = port.portName;
        qDebug() << "Opening port " << targetPortName;
        target->setPortName(targetPortName);
        if (target->open(QIODevice::ReadWrite)) {
            //For some reason the port doesn't work if you don't manually set the Dtr line
            target->setDtr(true);
            qDebug() << "Sucessfully opened port" << targetPortName;
            //If the device is already in Raw Register-Access mode, X exits
            sendCommand("X");
            //Go back to the root of the command list
            sendCommand("/");
            //Ask for raw register access and see if there is a suitable response
            if ( sendCommand("ra") ) {
                qDebug() << "Chose port " << target->portName();
                targetFound = true;
                return true;
            } else {
                //Use terminate() instead of close()
                // the flush() in close hangs on disconnected / nonexistent ports, terminate() skips that
                target->terminate();
                target->setPortName("");
            }
        } else {
            qDebug() << "Could not open port" << targetPortName;
            target->terminate();
            target->setPortName("");
        }
    }
    return false;
}

bool TargetComms::writeRegister(RegisterDef reg, QString value)
{
    //Depending on the register type, format the string appropriately
    QString fmValue;
    switch ( reg.regtype() ) {
        case REG_FLOAT:
            fmValue = QString("%1").arg(value.toFloat(), 5, 'f', 5, '0');
            break;
        case REG_INTEGER:
            fmValue = QString("%1").arg(value.toInt(), 5, 'f', 0, '0');
            break;
        case REG_STRING:
            fmValue = value;
            break;
        default:
            fmValue = "0";
            break;
    }

    if (target->isOpen() && reg.writeable()) {
        if (!value.isEmpty()) {
            //The command format is "W{register address to 5 digits}{space}{value to five digits}"
            QString command = QString("W%1").arg(reg.address(), 5, 'f', 0, '0').append(" ").append(fmValue);
            //qDebug() << "Sending command" << command << "to write register" << reg.name();
            if (sendCommand(command)) {
                //If the command succeeded update the master register list
                (*registerList)[registerList->indexOf(reg)].setValue(value);
                qDebug() << "Register " << registerList->at(registerList->indexOf(reg)).name() << " set to " << registerList->at(registerList->indexOf(reg)).getValue();
                return true;
            }
        }
    }
    return false;
}

bool TargetComms::readRegister(RegisterDef reg)
{
    if (target->isOpen() && reg.readable()) {
        //The request format is "R{register address to 5 digits}"
        QString request = QString("R%1").arg(reg.address(), 5, 'f', 0, '0');
        QByteArray response = sendRequest(request);
        if (!response.isEmpty() && response.startsWith("R")) {
            //Request was successful, response format is "R{register value}"
            // update the master register list
            QString readValue = response.remove(0,1).trimmed();
            (*registerList)[registerList->indexOf(reg)].setValue(readValue);
            //qDebug() << "Register" << registerList->at(registerList->indexOf(reg)).name() << "read to be" << registerList->at(registerList->indexOf(reg)).getValue();
            return true;
        }
    }
    //Something went wrong - for debugging, set the value to crazy, but for actual operation this could be skipped
    // then the register would retain its previous value
    (*registerList)[registerList->indexOf(reg)].setValue("!!!");
    return false;
}

bool TargetComms::readRegisterList()
{
    RegisterDef iRegister;
    if (target->isOpen()) {
        //Request format is just an "L"
        //Reply is "L{register name}={register flags},{register address} for each register"
        QList<QByteArray> rawRegisterList = sendRequest("L").split('\x0D');
        if ( !rawRegisterList.isEmpty() ) {
            foreach (QByteArray rawRegister, rawRegisterList) {
                if (rawRegister.startsWith("L")) {
                    //If the response looks sensible, create a RegisterDef and add it to our master register list
                    rawRegister = rawRegister.remove(0, 1).trimmed();
                    QString registerName = rawRegister.split('=').at(0);
                    int registerFlags = rawRegister.split('=').at(1).split(',').at(0).toInt();
                    int registerAddress = rawRegister.split('=').at(1).split(',').at(1).toInt();
                    iRegister = RegisterDef(registerName, registerAddress, registerFlags);
                    if (iRegister.readable() && !iRegister.hidden())
                        registerList->append(iRegister);
                }
            }
            if ( !registerList->isEmpty() ) {
                qDebug() << "Successful register list.";
                return true;
            }
        }
    }
return false;
}

QByteArray TargetComms::sendRequest(QString request)
{
    //Requests need the response back
    //If the request was successful the last line should be "A"
    QByteArray response = targetReadWrite(request);
    if (response.endsWith("A")) {
        response.chop(1);
        return response.trimmed();
    } else {
        emit errorMessage(QString("Recived unknown response to request %2: %1").arg(QString(response.simplified())).arg(request.simplified()));
    }
    return "";
}

bool TargetComms::sendCommand(QString command)
{
    //Commands don't need the response back
    // simply return whether the command succeeded (ends with an "A")
    QByteArray response = targetReadWrite(command);
    if (response.endsWith("A")) {
        return true;
    } else {
        emit errorMessage(QString("Recived unknown response to command %2: %1").arg(QString(response.simplified())).arg(command.simplified()));
    }
    return false;
}

QByteArray TargetComms::targetReadWrite(QString data)
{
    //Main communications function
    //Append a CR (ascii #13) to the end of the command and write it to the device
    // for some reason it doesn't like LF (ascii #10) or CR/LF as you would expect
    QByteArray byteData = data.toAscii();
    byteData.append('\x0D');
    //qDebug() << "Writing data" << byteData.trimmed().simplified() << " to target";
    target->write(byteData);
    QByteArray readData = "";
    qint64 bytesToRead;
    //wait for the response
    while (target->waitForReadyRead(50)) {
        //keep reading until there is radio silence (i.e. waitForReadyRead timed out instead of returning true)
        // as the response may arrive in several sections if it is e.g. the register list
        bytesToRead = target->bytesAvailable();
        readData += target->read(bytesToRead);
    }
    //response is blank - something went wrong - increment our error counter
    if (!readData.isEmpty()) {
        if (errorCounter > 0)
            --errorCounter;
        //qDebug() << "Read data" << readData.trimmed().simplified() << "in reply";
        return readData.trimmed();
    } else {
        ++errorCounter;
    }
    return "";
}

bool TargetComms::getLogData()
{
    //Request for log data is a "D"
    //Response is "D{register name}={register value}"
    // save the response to the currently active log file, whether that is the harness log or the flash log
    QMutexLocker lock(fileMux);
    QByteArray rawLogData = sendRequest("D");
    if (!rawLogData.isEmpty()) {
        QList<QByteArray> logItems = rawLogData.split('\x0D');
        foreach (QByteArray logItem, logItems) {
            if (logItem.startsWith("D")) {
                logData << logItem.trimmed().remove(0,1);
                logData << endl;
            } else {
                qDebug() << "Received incomplete log data.";
            }
        }
        return true;
    }
    return false;
}

//Main comms thread logic - simply cycle through the queue of items waiting to be processed
void TargetComms::run()
{
    forever {
        mutex->lock();
        if (quit)
            return;
        if (targetFound) {
            //our error counter is too high - we must have lost contact with the device
            // let the GUI know we lost the device
            if (errorCounter > MAX_ERRORS) {
                errorCounter = 0;
                targetFound = false;
                tryConnectionAgain = false;
                gotRegisterList = false;
                target->terminate();
                emit targetLost();
            }
            //fetch the register list if needed
            if (!gotRegisterList) {
                gotRegisterList = readRegisterList();
                if (!gotRegisterList)
                    target->flush();
                emit registerListReady(gotRegisterList);
            }
            //process the write queue, attempting to write each register a maximum of 5 times
            while (!writeQueue.isEmpty()) {
                RegisterDef reg = writeQueue.takeFirst();
                for (register int i=0; i < 5; ++i) {
                    if (writeRegister(reg, reg.getValue())) {
                        emit registerUpdated(reg);
                        break;
                    } else {
                        msleep(1000);
                    }
                }
            }
            //process the read queue, attempting to read each register a maximum of 2 times
            while (!readQueue.isEmpty()) {
                RegisterDef reg = readQueue.takeFirst();
                for (register int i=0; i < 2; ++i) {
                    if (readRegister(reg)) {
                        emit registerUpdated(reg);
                        break;
                    } else {
                        msleep(500);
                    }
                }
            }
            //if harness logging is on, get the latest burst of data
            if (logStatus) {
                getLogData();
            }
            //if a flash dump is needed, keep fetching log data till there is no more
            if (dumpNeeded) {
                int emptyCounter = 0;
                forever {
                    //keep trying until we get 5 empty responses
                    if (getLogData()) {
                        emit errorMessage("Dumping log data to disk...");
                    } else {
                        ++emptyCounter;
                    }
                    if (emptyCounter > 5)
                        break;
                }
                closeLogFile();
                dumpNeeded = false;
                //Let the GUI know we finished the dump
                emit dumpFinished();
            }
        } else {
            //We aren't connected to the device - do we need to try again?
            if (tryConnectionAgain) {
                tryConnectionAgain = connectTarget();
                if (!tryConnectionAgain)
                    //get rid of any leftover input or output
                    target->flush();
                //Let the GUI know what happened
                emit connectedToTarget(tryConnectionAgain, targetPortName);
            }
        }
        //Wait for something to do, or 1000msecs for the logging
        condition.wait(mutex, 1000);
        mutex->unlock();
    }
}
